package com.nulldev.util.web.YouTubeExtractor.extractors.ytapi_rolling;

import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.CLIENT_VERSION;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_CL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_LABEL;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.PAGE_STS;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.UTC_OFFSET;
import static com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants.VARIANTS_CHECKSUM;
import static com.nulldev.util.web.YouTubeExtractor.util.YouTubeExtractorUtil.getYTString;

import java.net.NoRouteToHostException;
import java.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.regex.Pattern;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.VariableAPI.StringsUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.concurrency.threadIt.v4.emapi.ExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.IExecutorManager;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.variable;
import com.nulldev.util.data.Arrays.arrays.FastLists;
import com.nulldev.util.data.Arrays.maps.FastMaps;
import com.nulldev.util.data.Date.Dates;
import com.nulldev.util.io.FastIO.FastIO;
import com.nulldev.util.languages.json.uniapi.UniJsonIterableValue;
import com.nulldev.util.languages.json.uniapi.UniJsonValue;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.web.HttpClient.HttpClient;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.URLUtils.Entry;
import com.nulldev.util.web.URLUtils.URLUtils;
import com.nulldev.util.web.URLUtils.fuckTracking.FuckTracking;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.YouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.YoutubeExtractionOptions;
import com.nulldev.util.web.YouTubeExtractor.clarifications.Clarification;
import com.nulldev.util.web.YouTubeExtractor.comments.YTComments.YTCommentInitData;
import com.nulldev.util.web.YouTubeExtractor.endscreen.EndscreenEntry;
import com.nulldev.util.web.YouTubeExtractor.endscreen.EndscreenEntry.EntryType;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractor;
import com.nulldev.util.web.YouTubeExtractor.extractors.IYouTubeExtractorConstants;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.SharedExtractionFunctions;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.SharedExtractorUtil;
import com.nulldev.util.web.YouTubeExtractor.extractors.util.YTIAPIClientType;
import com.nulldev.util.web.YouTubeExtractor.formats.AudioCodec;
import com.nulldev.util.web.YouTubeExtractor.formats.FileType;
import com.nulldev.util.web.YouTubeExtractor.formats.Format;
import com.nulldev.util.web.YouTubeExtractor.formats.VideoCodec;
import com.nulldev.util.web.YouTubeExtractor.items.YouTubeStoryboardParser;
import com.nulldev.util.web.YouTubeExtractor.items.YouTubeStoryboardParser.YouTubeStoryboard;
import com.nulldev.util.web.YouTubeExtractor.items.YouTubeVideoHeatmap;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata.DRMMetadata;
import com.nulldev.util.web.YouTubeExtractor.metadata.YouTubeMetadata.ThumbnailSizeOption;
import com.nulldev.util.web.YouTubeExtractor.playlist.Playlist;
import com.nulldev.util.web.YouTubeExtractor.playlist.Playlist.PlaylistEntry;
import com.nulldev.util.web.YouTubeExtractor.signatures.extractor.Extractor;
import com.nulldev.util.web.YouTubeExtractor.subtitles.YouTubeCaptions;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractedVideo;
import com.nulldev.util.web.YouTubeExtractor.video.ExtractionResult;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo;
import com.nulldev.util.web.YouTubeExtractor.video.YouTubeVideo.FormatType;

public class YouTubeAPIExtractorRolling extends IYouTubeExtractor {

	private static String generateURL(final String videoID, final Map<String, Object> arguments) {
		final StringBuilder b = new StringBuilder("https://www.youtube.com/watch?v=");
		b.append(videoID);
		if (arguments.containsKey(MAP_TYPE_FORCE_ENGLISH_LANG_COMPAT))
			b.append("&hl=EN");
		if (arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS)) {
			b.append("&bpctr=");
			b.append(System.currentTimeMillis());
		}
		b.append("&pbj=1");
		return b.toString();
	}

	private static final IExecutorManager td = ExecutorManager.get();
	private static final Logger logger = LoggerFactory.getLoggerD(true, true);
	public static final String VERSION = "0.1.13";

	private class ExtractedRequest {
		public final String result;
		public final String cookiePayload;

		public ExtractedRequest(final String result, final String cookiePayload) {
			this.result = result;
			this.cookiePayload = cookiePayload;
		}
	}

	@Override
	public Executable<ExtractedVideo> generateTask(String videoID, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(videoID, null, options);
	}

	@Override
	public Executable<ExtractedVideo> generateTask(String videoID, String extraArgs, YoutubeExtractionOptions... options) {
		logger.warn("generateTask(String, String, YoutubeExtractionOptions) is deprecated and marked for removal.");
		return generateTask(IYouTubeExtractorConstants.convertFromLegacy(videoID, extraArgs, options));
	}

	@Override
	public Executable<ExtractedVideo> generateTask(final Map<String, Object> arguments) {
		if (!isValidMap(arguments))
			throw new IllegalArgumentException("Invalid map: " + arguments);
		final String videoID = (String) arguments.get(MAP_TYPE_VIDEO_ID);
		if (!isValidVideoID(videoID))
			throw new IllegalArgumentException("Invalid video ID: " + videoID);
		final boolean use_v9_deciphering = !isEnabled(arguments, MAP_TYPE_DISABLE_V9_DECIPHERING);
		return td.makeTask(() -> {
			final List<YouTubeVideo> results = FastLists.list(32);
			final variable<Integer> _index = new variable<Integer>(-1);
			final Object _result = td.submit(() -> {
				logger.debug("=> Generating URL...");
				final StringBuilder extras = new StringBuilder();
				if (arguments.containsKey(MAP_TYPE_PLAYLIST_STR)) {
					extras.append("&list=");
					extras.append(arguments.get(MAP_TYPE_PLAYLIST_STR));
				}
				if (arguments.containsKey(MAP_TYPE_PLAYLIST_INDEX)) {
					extras.append("&index=");
					final int ix = (int) arguments.get(MAP_TYPE_PLAYLIST_INDEX);
					extras.append(ix);
					_index.set((int) arguments.get(MAP_TYPE_PLAYLIST_INDEX));
				}
				final URL url = URLUtils.parseURL(generateURL(videoID, arguments) + extras);
				logger.debug("URL: " + url);
				Entry[] _args = (!isEnabled(arguments, MAP_TYPE_DISABLE_CONSENT_COOKIE_COMPAT)) ? YouTubeExtractor.DEFAULT_COOKIES : null;
				if (isEnabled(arguments, MAP_TYPE_SEND_FAKE_COOKIES_COMPAT) || isEnabled(arguments, MAP_TYPE_ENFORCE_PRIVACY_MODE_COMPAT)) {
					if (_args == null)
						_args = new Entry[0];
					_args = ArrayUtils.add(_args, new Entry("Cookie", StringsUtil.randomString(64)));
				}
				if (isEnabled(arguments, MAP_TYPE_ENFORCE_PRIVACY_MODE_COMPAT) || isEnabled(arguments, MAP_TYPE_EMULATE_INSECURE_PROXY_COMPAT)) {
					if (_args == null)
						_args = new Entry[0];
					_args = ArrayUtils.addAll(_args, FuckTracking.protect());
				}
				final boolean SEND_XGOOG_API_KEY = isEnabled(arguments, MAP_TYPE_SEND_X_GOOG_KEY);
				String cookiePayload = "";
				try {
					logger.debug("=> Downloading metadata...");
					final HttpResponse<String> x = HttpClient.getString().url(url).timeout(Duration.ofSeconds(20)).headers(_args)
							.header("X-Goog-Api-Key", SEND_XGOOG_API_KEY ? YouTubeExtractorConstants.API_KEY : null)
							.header(new Entry("Referrer", "https://www.youtube.com/watch?v=" + videoID))
							.header(new Entry("X-SPF-Previous", "https://www.youtube.com/watch?v=" + videoID))
							.header(new Entry("X-SPF-Referer", "https://www.youtube.com/watch?v=" + videoID)).header("X-YouTube-Client-Name", "1")
							.header("X-YouTube-Client-Version", CLIENT_VERSION).header("X-YouTube-Page-CL", PAGE_CL).header("X-YouTube-Page-Label", PAGE_LABEL)
							.header("X-YouTube-STS", PAGE_STS).header("X-YouTube-Utc-Offset", UTC_OFFSET)
							.header("X-YouTube-Variants-Checksum", VARIANTS_CHECKSUM).create();
					for (final String cookie : x.cookies()) {
						cookiePayload += Splitters.splitString(cookie, ";")[0] + ";";
					}
					if (cookiePayload.endsWith(";")) {
						cookiePayload = cookiePayload.substring(0, cookiePayload.length() - 1);
					}
					return new ExtractedRequest(StringsUtil.replace(x.dataThenClose(), "\u0008", ""), cookiePayload);
				} catch (Exception e) {
					if (e instanceof NoRouteToHostException) {
						return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
								.setYTMessage("No route to youtube.com was found, this might be the internet.");
					}
					return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_UNABLE_TO_GET_METADATA")
							.setYTMessage("Hit the following exception: " + e.getMessage());
				}
			}).waitTillCompletion();
			if (_result instanceof ExtractedVideo) {
				return ((ExtractedVideo) _result);
			}
			final String cookiePayload = ((ExtractedRequest) _result).cookiePayload;
			final String result = ((ExtractedRequest) _result).result;
			if (arguments.containsKey(MAP_TYPE_ENABLE_DEBUG_FULL_RESPONSE_PRINT)) {
				JVM.println(result);
			}
			final UniJsonValue objs = UniJsonValue.parseArray(result);
//			final UniJsonValue _playerResponse = objs.arrayGetContains("playerResponse");
			final UniJsonValue response = objs.arrayGetContains("response");
//			final UniJsonValue player = objs.arrayGetContains("player");
			final Executable<UniJsonValue> playerResponse = td.submit(() -> {
				return objs.arrayGetContains("playerResponse").get("playerResponse"); // UniJsonValue.parse(player.get("player").get("args").getString("player_response"),
																						// true);
			});
//			String _playerURL = player.getSafe("player").getSafe("assets").getString("js", null); //TODO: Add signature hash checking.
//			logger.trace("playerURL: " + _playerURL);
			logger.debug("=> Accquiring decipherer.");
			final Executable<Extractor> e = td.submit(() -> {
				try {
					return YouTubeExtractor.extractor.getAuto();
				} catch (Exception ef) {
					throw new RuntimeException(ef);
				}
			});
			final UniJsonValue metadataCore = playerResponse.waitTillCompletion();
			if (metadataCore == null || metadataCore.isNull()) {
				return new ExtractedVideo(null, null, ExtractionResult.FAILED).setMessage("FAILED_STAGE_GET_METADATA_CORE")
						.setYTMessage("Either this video is private or was removed.");
			}
			if (arguments.containsKey(MAP_TYPE_ENABLE_DEBUG_API_RESPONSE_PRINT)) {
				JVM.println(playerResponse.result());
			}
			if (metadataCore.contains("playabilityStatus")) {
				if (metadataCore.get("playabilityStatus").contains("status")) {
					final String stat = metadataCore.get("playabilityStatus").getString("status", "OK");
					if (!stat.equalsIgnoreCase("OK")) {
						if (stat.equalsIgnoreCase("LOGIN_REQUIRED")) {
							return new ExtractedVideo(null, null, ExtractionResult.FAILED_AGE_RESTRICTED).setMessage("FAILED_VIDEO_AGE_RESTRICTED")
									.setYTMessage("YouTube reports this video as being age-gated.");
						} else {
							logger.error("Unknown playability status: " + stat);
						}
					}
				}
			}
			final UniJsonValue metadataObject = metadataCore.get("videoDetails");
			final variable<Playlist> pl = new variable<Playlist>(null);
			final Executable<List<EndscreenEntry>> endscreen = td.submit(() -> {
				final List<EndscreenEntry> entries = FastLists.list();
				if (metadataObject.hasValue("endscreen")) {
					for (final UniJsonValue _v : metadataObject.get("endscreen").get("endscreenRenderer").get("elements").asArray()) {
						final UniJsonValue v = _v.get("endscreenElementRenderer");
						final String style = v.getString("style");
						if (style == null) {
							logger.error("Failed to get endscreen style.");
							continue;
						}
						if (style.equalsIgnoreCase("CHANNEL")) {
							continue; // Rejected implementation
						} else if (style.equalsIgnoreCase("PLAYLIST")) {
							try {
								final String fullURL = "/watch?v=" + v.get("endpoint").get("watchEndpoint").getString("videoId") + "&list="
										+ v.get("endpoint").get("watchEndpoint").getString("playlistId");
								entries.add(new EndscreenEntry(EntryType.PLAYLIST, fullURL, v.get("endpoint").get("watchEndpoint").getString("videoId"),
										getYTString(v.get("title")), getYTString(v.get("metadata"))));
							} catch (Exception ef) {
								logger.error("Failed to process PLAYLIST endscreen entry", ef);
								logger.info(v);
							}
						} else if (style.equalsIgnoreCase("VIDEO")) {
							try {
								final String fullURL = "/watch?v=" + v.get("endpoint").get("watchEndpoint").getString("videoId");
								entries.add(new EndscreenEntry(EntryType.VIDEO, fullURL, v.get("endpoint").get("watchEndpoint").getString("videoId"),
										v.get("title").getString("simpleText"), v.get("metadata").getString("simpleText")));
							} catch (Exception ef) {
								logger.error("Failed to process VIDEO endscreen entry", ef);
								logger.info(v);
							}
						} else if (style.equalsIgnoreCase("WEBSITE")) {
							try {
								final String fullURL = v.get("endpoint").get("urlEndpoint").getString("url")
										.replaceAll(Pattern.quote("https://www.youtube.com"), "");
								final String thumbnail = v.get("image").get("thumbnails").get(0).getString("url");
								entries.add(new EndscreenEntry(EntryType.WEBSITE, fullURL, "null", v.get("title").getString("simpleText"),
										v.get("metadata").getString("simpleText")).overrideImageURL(thumbnail));
							} catch (Exception ef) {
								logger.error("Failed to process WEBSITE endscreen entry", ef);
								logger.info(v);
							}
						} else {
							logger.warn("Ignored unknown endscreen style: " + style);
							logger.warn(v.toString());
						}
					}
				}
				return entries;
			});
			final Executable<YouTubeMetadata> metadata = td.submit(() -> {
				final List<YouTubeCaptions> captions = FastLists.list();
				if (playerResponse.waitTillCompletion().contains("captions")) {
					// captions found
					final UniJsonValue ct = playerResponse.waitTillCompletion().getSafe("captions").getSafe("playerCaptionsTracklistRenderer")
							.getIgnoreNull("captionTracks");
					if (ct == null) {
						logger.error("Failed to load captions, captionTracks was null."); // probably no subtitles or contributions are only enabled.
					} else {
						final UniJsonIterableValue captionArray = ct.asArray();
						if (!captionArray.isNull()) {
							for (final UniJsonValue _v : captionArray) {
								final UniJsonValue caption = _v;
								final String captionURL = caption.getString("baseUrl");
								final String captionText = StringsUtil._capitalizeString(caption.get("name").getString("simpleText"));
								final String captionCode = caption.getString("languageCode");
								captions.add(new YouTubeCaptions(captionURL, captionText, captionCode));
							}
						}
					}
				}
				UniJsonValue keywords = UniJsonValue.array();
				try {
					keywords = metadataObject.get("keywords").asArray();
				} catch (Exception _e) {
				}
				if (keywords == null) {
					logger.warn("Fixed keywords!");
					keywords = UniJsonValue.array();
				}
				final boolean is360 = /*
										 * result.contains("360°") ||
										 */ SharedExtractorUtil.is360(result);
				final YouTubeMetadata _metadata = new YouTubeMetadata(videoID, metadataObject.getString("title", "null").replaceAll(Pattern.quote("%3B"), ";"),
						metadataObject.getString("author", "null").replaceAll(Pattern.quote("%3B"), ";"), metadataObject.getString("channelId", "null"),
						metadataObject.getLong("viewCount", -1), metadataObject.getLong("lengthSeconds", -1), metadataObject.getDouble("averageRating", 5), -1,
						-1, is360, keywords.toArray(), captions);
				_metadata.setVR180(is360 && true); // FIXME: Proper soluton.
				_metadata.limitedExtraction(result.contains("verify_controversy") || arguments.containsKey(MAP_TYPE_ENABLE_CONTROVERSY_BYPASS));
				if (metadataObject.contains("thumbnail")) {
					if (metadataObject.get("thumbnail").contains("thumbnails") && metadataObject.mget("thumbnail.thumbnails").isArray()) {
						int level = 0;
						for (final UniJsonValue v : metadataObject.mget("thumbnail.thumbnails").asArray()) {
							final String url = v.getString("url", null);
							if (url == null || url.isEmpty()) {
								logger.warn("Invalid thumbnail object: " + v);
								continue;
							}
							final int localLevel = ThumbnailSizeOption.getLevel(url);
							if (level < localLevel)
								level = localLevel;
						}
						_metadata.setThumbnailLevel(level);
					}
				}
				if (metadataCore.hasValue("microformat") && metadataCore.get("microformat").hasValue("playerMicroformatRenderer")) {
					try {
						final UniJsonValue mf = metadataCore.get("microformat").get("playerMicroformatRenderer");
						_metadata.getExtendedMetadata().uploadDate = mf.getString("uploadDate");
						_metadata.getExtendedMetadata().publishDate = mf.getString("publishDate");
						_metadata.getExtendedMetadata().isFamilyFriendly = mf.getBoolean("isFamilySafe");
						if (metadataCore.hasValue("playerConfig") && metadataCore.get("playerConfig").hasValue("audioConfig")) {
							final UniJsonValue pc = metadataCore.get("playerConfig").get("audioConfig");
							_metadata.getExtendedMetadata().loudnessDbRaw = pc.getFloat("loudnessDb", 0);
							_metadata.getExtendedMetadata().perceptualLoudnessDb = pc.getFloat("perceptualLoudnessDb", 0);
							if (_metadata.getExtendedMetadata().loudnessDbRaw == 0) {
								_metadata.getExtendedMetadata().loudnessDbRaw = pc.getFloat("perceptualLoudnessDb", 0);
							}
						}
					} catch (Exception ef) {
						logger.error("Failed to get extended data!", ef);
					}
				}
				_metadata.setMap(result);
				_metadata.setDescription(StringsUtil.replaceEach(metadataObject.getString("shortDescription", "A description wasn't found."),
						DESCRIPTION_REPLACEMENTS_INPUT, DESCRIPTION_REPLACEMENTS_OUTPUT));
				return _metadata;
			});
			if (metadataObject.hasValue("isUpcoming") && metadataObject.getBoolean("isUpcoming")) {
				logger.error("Failed -> This video isn't live yet!");
				final String liveChatCont = StringsUtil.substringBetweenO(result,
						"\"conversationBar\":{\"liveChatRenderer\":{\"continuations\":[{\"reloadContinuationData\":{\"continuation\":\"",
						"\",\"clickTrackingParams\":\"");
				final long time = playerResponse.waitTillCompletion().get("playabilityStatus").get("liveStreamability").get("liveStreamabilityRenderer")
						.get("offlineSlate").get("liveStreamOfflineSlateRenderer").getLong("scheduledStartTime", -1);
				final String times = TimeUtil.formatSecondsL(time - System.currentTimeMillis() / 1000);
				metadata.waitTillCompletion().setContentStartTime((long) (time * 1000));
				return new ExtractedVideo(results, metadata.waitTillCompletion().setEndscreenData(endscreen.waitTillCompletion()), ExtractionResult.FAILED)
						.setMessage("ERROR_UPCOMING_STREAM").setYTMessage("This video or stream will be live in " + times + " from now. (Stream Start: "
								+ Dates.time((long) (time * 1000)).toString() + ")")
						.setChatPayload(liveChatCont);
			}
			if (playerResponse.waitTillCompletion().hasValue("streamingData")) {
				final UniJsonValue streamingData = playerResponse.result().get("streamingData");
				final String liveChatCont = StringsUtil.substringBetweenO(result,
						"\"conversationBar\":{\"liveChatRenderer\":{\"continuations\":[{\"reloadContinuationData\":{\"continuation\":\"",
						"\",\"clickTrackingParams\":\""); // FIXME: Switch to 3->response->contents->twoColumnWatchNextResults->
				// conversationBar->liveChatRenderer->continuations[0]->reloadContinuationData->continuation
				if (streamingData.hasValue("hlsManifestUrl")) {
					metadata.waitTillCompletion().setLiveStream(true);
					logger.debug("Parsing a live video...");
					final String hlsMetadata = streamingData.getString("hlsManifestUrl");
					results.clear();
					results.add(new YouTubeVideo(hlsMetadata, false, 0, new Format(0, FileType.M3U8, 30, 720, VideoCodec.H264, AudioCodec.AAC, -1, false))
							.setLivestream(true).setInternalProxy(true).setFormatType(FormatType.M3U8_STREAM));
					return new ExtractedVideo(results, metadata.waitTillCompletion(), ExtractionResult.SUCCESS).setChatPayload(liveChatCont);
				}
			}
			final boolean allowLateExtractions = isEnabled(arguments, MAP_TYPE_ENABLE_LATE_EXTRACTIONS);
			final variable<Boolean> isVR180 = new variable<Boolean>(false);
			final BlockingQueue<Executable<YouTubeVideo>> queue = new LinkedTransferQueue<Executable<YouTubeVideo>>();
			if (playerResponse.waitTillCompletion().hasValue("streamingData")) {
				final UniJsonValue streamingData = playerResponse.result().get("streamingData");
				streamingData.get("formats").asArray().forEach((v) -> {
					SharedExtractionFunctions.generate_legacy_video(queue, results, v, e, isVR180, use_v9_deciphering, true);
				});
				streamingData.get("adaptiveFormats").asArray().forEach((v) -> {
					SharedExtractionFunctions.generate_adaptive_video(queue, results, v, e, isVR180, use_v9_deciphering, true);
				});
				if (!allowLateExtractions)
					td.awaitQueue(queue);
				if (streamingData.contains("dashManifestUrl")) {
					results.add(
							new YouTubeVideo(streamingData.getString("dashManifestUrl"), false, -1, new Format(-1, FileType.UNKNOWN, AudioCodec.NONE, 0, false))
									.setFormatType(FormatType.DASH_STREAM));
				}
			}
			if (metadataCore.contains("storyboards")) {
				if (metadataCore.get("storyboards").contains("playerStoryboardSpecRenderer")) {
					String spec = metadataCore.mget("storyboards.playerStoryboardSpecRenderer").getString("spec", null);
					metadata.waitTillCompletion().setRawStoryboardURL(spec);
					if (spec != null) {
						try {
							final YouTubeStoryboard sb = YouTubeStoryboardParser.parse(spec);
							logger.trace("abi(5000): " + sb);
							metadata.waitTillCompletion().setStoryboardURL(sb.urlFor(sb.section(sb.getBestSectionSize())));
						} catch (Exception ex) {
							logger.error("Failed to decipher storyboard!", ex);
						}
					}
				} else if (metadataCore.get("storyboards").contains("playerLiveStoryboardSpecRenderer")) {
					String spec = metadataCore.mget("storyboards.playerLiveStoryboardSpecRenderer").getString("spec", null);
					metadata.waitTillCompletion().setRawStoryboardURL(spec);
					if (spec != null) {
						try {
							final YouTubeStoryboard sb = YouTubeStoryboardParser.parse(spec);
							logger.trace("abi(5000): " + sb);
							metadata.waitTillCompletion().setStoryboardURL(sb.urlFor(sb.section(sb.getBestSectionSize())));
						} catch (Exception ex) {
							logger.error("Failed to decipher storyboard!", ex);
						}
					}
				}
			}
			if (response != null) {
				try {
					final UniJsonValue o = response.get("response").get("contents").get("twoColumnWatchNextResults");
					if (o.hasValue("playlist")) {
						final UniJsonValue p = o.get("playlist").get("playlist");
						int currentIndex = p.getInt("currentIndex", -1);
						if (currentIndex == -1) {
							logger.error("Failed to track playlist index!");
						} else {
							logger.debug("current playlist index: " + currentIndex);
							pl.set(new Playlist(p.getString("title"), currentIndex));
							for (final UniJsonValue _v : p.get("contents").asArray()) {
								if (_v.hasValue("playlistPanelVideoRenderer")) {
									final UniJsonValue v = _v.get("playlistPanelVideoRenderer");
									final String _videoID = v.getString("videoId");
									final int index = v.get("navigationEndpoint").get("watchEndpoint").getInt("index");
									final String url = "/watch?v=" + _videoID + "&list="
											+ v.get("navigationEndpoint").get("watchEndpoint").getString("playlistId") + "&index=" + (index + 1);
									String title = "[FIXME] Unknown.";
									if (v.hasValue("title")) {
										if (v.get("title").hasValue("simpleText")) {
											title = v.get("title").getString("simpleText");
										} else {
											logger.warn("Missing simpleText in title object!");
											logger.warn(v.toString());
										}
									}
									String author = "[FIXME] Unknown.";
									if (v.hasValue("longBylineText")) {
										if (v.get("longBylineText").hasValue("runs")) {
											author = v.get("longBylineText").get("runs").get(0).getString("text");
										} else {
											logger.warn("Missing text in longBylineText.runs object!");
											logger.warn(v.toString());
										}
									}
									pl.object().addEntry(new PlaylistEntry(title, "https://i.ytimg.com/vi/" + _videoID + "/mqdefault.jpg", author, url, index));
								} else {
									continue;
								}
							}
							pl.object().makeCurrent(pl.object().at(currentIndex));
							try {
								pl.object().makeNext(pl.object().at(currentIndex + 1));
							} catch (Exception eo) {
								logger.error("Failed to get next playlist value!", eo);
							}
							logger.debug("Found playlist!");
						}
					}
				} catch (Exception ef) {
					logger.error("[playlistGen] Failed to get ytInitialData!", ef);
				}
				try {
					metadata.waitTillCompletion().setChapters(SharedExtractorUtil.getChapters(response.get("response")));
				} catch (Exception ef) {
					logger.error("[chapterGen] Failed to get ytInitialData!", ef);
				}
				try {
					UniJsonValue supertags = response.get("response").mget("contents.twoColumnWatchNextResults.results.results.contents");
					if (supertags != null && !supertags.isNull()) {
						try {
							supertags = supertags.get(0).get("videoPrimaryInfoRenderer").get("superTitleLink").get("runs");
						} catch (Exception __e) {
						}
					}
					if (supertags != null && !supertags.isNull() && supertags.isArray()) {
						final List<String> supertags_entries = FastLists.list();
						supertags.asArray().forEach((tag) -> {
							final String text = tag.getString("text", "");
							if (!text.isEmpty() && !text.equals(" "))
								supertags_entries.add(text);
						});
						metadata.waitTillCompletion().setSupertags(supertags_entries);

					}
				} catch (Exception ef) {
					logger.error("[superTagGen] Failed to get ytInitialData!", ef);
				}
			}
			YTCommentInitData cmdid = null;
			if (response != null) {
				// x[3].response.contents.twoColumnWatchNextResults.results.results.contents[2].itemSectionRenderer.continuations[0].nextContinuationData.continuation
				final UniJsonValue _response = response.getJSONObject("response").getJSONObject("contents");
				final UniJsonValue twoColumnWatchNextResults = _response.getJSONObject("twoColumnWatchNextResults").getJSONObject("results")
						.getJSONObject("results");
				final UniJsonValue contents = twoColumnWatchNextResults.getJSONArray("contents");
				final UniJsonValue clarification = contents.first();
				if (clarification != null && !clarification.isNull() && isEnabled(arguments, MAP_TYPE_ENABLE_CLARIFICATIONS)) {
					if (clarification.get("itemSectionRenderer").get("contents").get(0).hasValue("clarificationRenderer")) {
						final UniJsonValue cr = clarification.get("itemSectionRenderer").get("contents").get(0).get("clarificationRenderer");
						final String title = cr.get("contentTitle").get("runs").get(0).getString("text");
						final String text = cr.get("text").get("runs").get(0).getString("text");
						final String subText = cr.get("source").getString("simpleText");
						final String url = cr.get("endpoint").get("urlEndpoint").getString("url");
						metadata.waitTillCompletion().addClarification(new Clarification(title, text, subText, url));
					}
				}
				final UniJsonValue commentMeta = contents.last().getJSONObject("itemSectionRenderer");
				try {
					String cont = null;
					try {
						if (commentMeta.contains("continuations")) {
							cont = commentMeta.get("continuations").asArray().get(0).get("nextContinuationData").getString("continuation");
						} else {
							cont = commentMeta.get("contents").asArray().get(0).get("continuationItemRenderer").get("continuationEndpoint")
									.get("continuationCommand").getString("token");
						}
					} catch (Exception fx) {
					}
					logger.debug("comment_cont: " + cont);
					if (cont != null) {
						cmdid = new YTCommentInitData(videoID, cookiePayload, "1", YouTubeExtractorConstants.CLIENT_VERSION, cont,
								response.getString("xsrf_token", YouTubeExtractorConstants.XSRF_TOKEN));
					}
				} catch (Exception e1) {
					// ASSUME comments are disabled.
				}
			}
			final Executable<?> drm = td.submit(() -> {
				if (metadataCore.contains("streamingData") && metadataCore.get("streamingData").contains("licenseInfos")) {
					logger.debug("[DRM] Found DRM, attempting to generate metadata.");
					final UniJsonValue dr = metadataCore.get("streamingData");
					final UniJsonValue lis = dr.get("licenseInfos");
					final DRMMetadata.LicenseInfo[] lifs = new DRMMetadata.LicenseInfo[lis.length()];
					for (int i = 0; i < lis.length(); i++) {
						final UniJsonValue li = lis.get(i);
						lifs[i] = new DRMMetadata.LicenseInfo(li.getString("url"), li.getString("drmFamily"));
					}
					final DRMMetadata drmM = new DRMMetadata(dr.getString("drmParams"), dr.getString("dashManifestUrl"), lifs);
					metadata.waitTillCompletion().setDRMMetadata(drmM);
					if (metadataCore.contains("playerConfig") && metadataCore.get("playerConfig").contains("webDrmConfig")) {
						final UniJsonValue drmConfig = metadataCore.get("playerConfig").get("webDrmConfig");
						drmM.setUseInnertubeDrmService(drmConfig.getBoolean("useInnertubeDrmService"));
						drmM.setWidevineCertificate(drmConfig.getString("widevineServiceCert"));
					}
				}
				return null;
			});
			td.await(drm);
			return new ExtractedVideo(results, metadata.waitTillCompletion().setVR180(isVR180.get()), ExtractionResult.SUCCESS)
					.setLateExtraction(allowLateExtractions).setLateExtractionHold(allowLateExtractions ? td.asyncAwait(queue) : null).setPlaylist(pl.object())
					.setCommentInitData(cmdid);
		});
	}

	@Override
	public Object getFlag(final int flag) {
		switch (flag) {
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAM_CHAT:
			case FLAG_EXTRACTOR_SUPPORTS_LIVESTREAMS:
			case FLAG_EXTRACTOR_SUPPORTS_LATE_EXTRACTION:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_SUPPORTS_ABI_2_2:
				return FLAG_TRUE;
			case FLAG_EXTRACTOR_GET_SUPPORTED_ABI:
				return 2.2f;
			case FLAG_EXTRACTOR_SUPPORTS_AGE_GATE:
				return FLAG_FALSE;
			default:
				return FLAG_NOT_SUPPORTED;
		}
	}

	@Override
	public String getID() {
		return "ytapi.native-rolling";
	}

	@Override
	public int abiVersion() {
		return 5020; // Supports 5100, 5300 and 5310.
	}

	public static void main(String[] args) {
		final IYouTubeExtractor ex = YouTubeExtractor.get("ytapi.native-rolling");
		ex.enableInstanceDebugging(true);
		final Executable<ExtractedVideo> f = ex.generateTask(FastMaps.fixedUniMap(new Object[][]
			{
					{ IYouTubeExtractorConstants.MAP_TYPE_VIDEO_ID, "QcBaSz3c3H8" },
					{ IYouTubeExtractorConstants.MAP_TYPE_ENABLE_DEBUG_API_RESPONSE_PRINT, true },
					{ IYouTubeExtractorConstants.MAP_TYPE_LIVESTREAMS_KEEP_RAW_STREAMS, true },
					{ IYouTubeExtractorConstants.MAP_TYPE_USE_CLIENT_TYPE, YTIAPIClientType.WEB },
					{ IYouTubeExtractorConstants.MAP_TYPE_DISABLE_V8_DECIPHERING, true } }));
		logger.traceEnabled.set(true);
		ExecutorManager.get().asyncSubmit(f);
		final long s0 = System.currentTimeMillis();
		JVM.println("result: " + f.waitTillCompletion());
		JVM.println("sync: " + f);
		final long e0 = System.currentTimeMillis();
		JVM.println("Execution Time: " + (e0 - s0) + " ms");
		if (f.result() == null) {
			throw new NullPointerException("Failed to get a result!");
		}
		if (f.result().supportsLateExtraction()) {
			f.result().awaitCompletion();
			JVM.println("lateExtraction: COMPLETE");
		}
		if (!f.result().getResult().successful()) {
			JVM.println("Failed to extract.");
			FastIO.get().flush();
			JVM.exit();
			return;
		}
		if (f.result().hasCommentData()) {
			JVM.println("commentData: " + f.result().getCommentData());
		}
		if (f.result().getMetadata().hasHeatmap()) {
			final Executable<YouTubeVideoHeatmap> exh = f.result().getMetadata().getHeatmap();
			JVM.println("heatmap: " + exh);
			if (exh.hasFailed()) {
				JVM.errorln(exh.getException());
			}
		}
		final long s = System.currentTimeMillis();
		if (f.result().getMetadata() != null)
			JVM.println("enhanced description: " + f.result().getMetadata().getEnhancedDescription());
		final long e = System.currentTimeMillis();
		JVM.println("lat: " + (e - s) + " ms");
		if (f.result() != null) {
			final List<YouTubeVideo> result = f.waitTillCompletion().videos();
			ArrayUtils.printLln(result);
			JVM.println(f.result().getMetadata());
		}
		FastIO.get().flush();
		JVM.exit();
	}

}
